JavaScript engines manage memory automatically through a combination of stack allocation for primitive values and function calls, and heap allocation with generational garbage collection for objects.
JavaScript engines like V8 (Chrome/Node.js), SpiderMonkey (Firefox), and JavaScriptCore (Safari) handle memory management automatically, freeing developers from manual allocation and deallocation. The memory model is divided into two primary regions: the stack and the heap, each with distinct characteristics and purposes. The heap is further organized using a generational approach to optimize garbage collection based on object lifetimes .
Purpose: The stack stores primitive values (numbers, booleans, strings, null, undefined, symbols) and references to heap objects, along with function call frames .
Management: Stack memory operates on a Last-In-First-Out (LIFO) basis. Each function call creates a new stack frame that is automatically destroyed when the function returns .
Performance: Stack allocation is extremely fast because it only involves moving a pointer. However, stack size is limited (typically ~1-8 MB per thread), and deep recursion can cause stack overflow errors .
Purpose: The heap stores all objects, arrays, functions, and closures—anything that requires dynamic memory allocation .
Generational Design: Modern engines use generational garbage collection based on the observation that most objects die young. The heap is divided into generations with different collection strategies .
Heap Structure in V8: V8 organizes the heap into several regions including: New Space (young generation), Old Space (old generation), Code Space (compiled code), Map Space (hidden classes), and Large Object Space (objects too large for other spaces) .
The generational garbage collection strategy is fundamental to JavaScript engine performance. Engines assume that most objects are temporary and die young, so they optimize for this common case by using different algorithms for different generations .
Purpose: Newly created objects are allocated here. This space is small (typically 1-8 MB) and collected frequently .
Scavenge Algorithm: V8 uses a semi-space copying collector. The young generation is split into two equal halves: From-space (active) and To-space (inactive). New objects go into From-space. During collection, live objects are copied to To-space, then the spaces are swapped .
SpiderMonkey's Nursery: Firefox's engine uses a similar concept called the 'nursery' for short-lived objects. During nursery collection, accessible objects are 'tenured' (moved to long-lived memory) and the nursery is cleared .
Performance Trade-off: Scavenge is fast because it only processes live objects and doesn't need to scan the entire heap, but it wastes half the space as idle .
Promotion: Objects that survive multiple young generation collections (typically 2 cycles) are moved (promoted) to the old generation .
Mark-Sweep Algorithm: The old generation uses mark-sweep collection. It starts from root objects (global object, stack variables, etc.), traverses the object graph, and marks all reachable objects. Unmarked objects are considered garbage and their memory is freed .
Mark-Compact: To combat memory fragmentation, mark-compact moves all live objects together, then frees the remaining space as one contiguous block. This is more expensive but necessary for large heaps .
Incremental Marking: To avoid long 'stop-the-world' pauses, V8 uses incremental marking, breaking the marking phase into small steps interleaved with program execution—similar conceptually to React Fiber .
Mark-and-Sweep: The fundamental algorithm. Starting from root objects (global object, current stack, registers), it traverses all references and marks every reachable object. Then it sweeps through memory, freeing unmarked objects. This handles circular references correctly .
Reference Counting: An older approach that tracks how many references point to each object. When count reaches zero, the object is freed. However, it fails with circular references (two objects referencing each other with no external references) and isn't used alone in modern engines .
Generational Collection: Combines the above with the insight that most objects die young. Young generation collected frequently with copying collector; old generation collected less frequently with mark-sweep/compact .
V8 (Chrome/Node.js): Uses Ignition interpreter and TurboFan compiler. Implements generational GC with Scavenge for young generation and mark-sweep/compact for old generation. Provides command-line flags like --max-old-space-size and --max-semi-space-size for tuning .
SpiderMonkey (Firefox): Also uses generational GC with a nursery for young objects. Has special handling for strings (deduplication, atomization) and uses a mark-and-sweep collector for the tenured heap. Embedders using the C++ API must use rooting mechanisms (JS::Rooted) to protect objects from garbage collection .
JavaScriptCore (Safari): Provides memory management through its JSVirtualMachine and JSManagedValue classes, with conditional retain behavior for automatic management. Can detect system RAM size to set allocation limits .
Global Variables: Variables attached to the global object remain reachable forever. Always use let, const, or module scope .
Forgotten Event Listeners: Listeners keep references to their callback functions and any variables closed over them. Remove listeners when no longer needed .
Closures: Inner functions that close over variables keep those variables alive as long as the function exists. Be mindful of what closures capture .
Detached DOM Elements: Holding references to removed DOM nodes prevents their memory from being freed .
Circular References: While modern GC handles cycles, circular references in combination with other patterns (like event listeners) can still cause leaks .
Understanding memory management helps developers write more efficient code. Tools like Chrome DevTools Memory panel and Node.js flags (--trace-gc, --expose-gc for manual triggering) allow inspection of memory behavior . Key takeaways: keep object shapes consistent (monomorphic) to help hidden classes, avoid creating many temporary objects in hot paths, and be mindful of closures that inadvertently capture large objects. The engine optimizes automatically, but informed developers can avoid patterns that force deoptimization or prevent efficient garbage collection .